from __future__ import with_statement

import sys
import struct
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO
try:
    import cPickle as pickle
except ImportError:
    import pickle
from kineta.error import KinetaError


class InvertKey(dict): # TODO: blog about how this works

    def __init__(self, d):
        for k, v in d.iteritems():
            self[k] = v

    def __str__(self): #pragma: no cover
        return "_InvertKey:%s" % dict.__str__(self)


class Largest(object):

    pass


class InternalRow(object):

    def __init__(self, joinname, row, rowid, size, clone=None):
        if clone is not None:
            self._dict = clone
        else:
            self._dict = {joinname: (row, rowid)}
        self.size = size

    def get_tuple(self, joinname):
        return self._dict[joinname]

    def get_value(self, joinname, col):
        t = self._dict.get(joinname, None)
        if t is not None:
            return t[0].get(col, None)
        return None

    def has_join(self, joinname):
        return joinname in self._dict

    def combine(self, internalrow):
        for k, v in internalrow._dict.iteritems():
            self._dict[k] = v
        self.size += internalrow.size

    def clone(self):
        return InternalRow(None, None, None, self.size, clone=dict(self._dict))

    def __repr__(self):
        return "IR: %s" % self._dict


def cmp_rows(columns, leftrow, rightrow):
    ret = 0
    leftint = None
    rightint = None
    for colspec in columns:
        if len(colspec) == 2:
            c, direction = colspec
            lv = leftrow.get(c, None)
            rv = rightrow.get(c, None)
        else:
            join, c, direction = colspec
            lv = leftrow.get_value(join, c)
            rv = rightrow.get_value(join, c)

        ret = cmp_values(lv, rv)

        if direction == 'd':
            ret *= -1

        if ret != 0:
            break
    if isinstance(leftrow, InvertKey):
        ret = -1 if ret < 0 else 1
    elif isinstance(rightrow, InvertKey): #pragma: no cover
        ret = 1 if ret > 0 else -1
    return ret


def cmp_values(lv, rv):
    if lv is None and rv is None:
        return 0
    elif lv is None:
        return -1
    elif rv is None:
        return 1

    lt = _get_type(lv)
    rt = _get_type(rv)

    if (lt is list) and (rt is list):
        return _cmp_lists(lv, rv)
    if lt is rt:
        return cmp(lv, rv)
    return _cmp_types(lt, rt)


def _get_type(v):
    t = type(v)
    if t is str:
        t = unicode
    elif (t is int) or (t is long):
        t = float
    elif t is tuple:
        t = list
    return t


def _cmp_lists(lv, rv):
    i = 0
    ll = len(lv)
    rl = len(rv)
    while i < ll and i < rl:
        r = cmp_values(lv[i], rv[i])
        if r != 0:
            return r
        i += 1
    return cmp(ll, rl)


typedict = {list: 0,
            bool: 1,
            float: 2,
            unicode: 3,
            Largest: 4}


def _cmp_types(lt, rt):
    li = typedict.get(lt, None)
    ri = typedict.get(rt, None)
    if li is None: #pragma: no cover
        raise KinetaError("Invalid value type %s" % lt)
    if ri is None: #pragma: no cover
        raise KinetaError("Invalid value type %s" % rt)
    return cmp(li, ri)


class HashableVal(object):

    def __init__(self, val):
        self.val = val

    def __hash__(self):
        if isinstance(self.val, (tuple, list)):
            ret = 0
            for v in self.val:
                ret += hash(v)
            return ret
        else:
            return hash(self.val)

    def __eq__(self, other):
        return cmp_values(self.val, other.val) == 0

    def __lt__(self, other):
        return cmp_values(self.val, other.val) < 0


def idx_key_factory(cols):
    return lambda key, data: dump_key(cols, pickle.loads(data))


# the below routines turn index keys into byte strings
# which when lexically compared (hopefully) match the semantics of
# the cmp_rows function.  This is done to avoid having to specify
# a comparison function to berkdb, because doing so is extremely slow

TYPE_TERM = '\x00'
TYPE_NONE = '\x01'
TYPE_LIST = '\x03'
TYPE_BOOL_FALSE = '\x06'
TYPE_BOOL_TRUE = '\x07'
TYPE_NEGATIVE_FLOAT = '\x09'
TYPE_POSITIVE_FLOAT = '\x0A'
TYPE_UNICODE = '\x0C'
TYPE_LARGEST = '\xFF'
pack_double = struct.Struct("!d")
pack_long = struct.Struct("!q")
TYPE_TERM_INV = chr(ord(TYPE_TERM) ^ 0xFF)
TYPE_NONE_INV = chr(ord(TYPE_NONE) ^ 0xFF)
TYPE_LIST_INV = chr(ord(TYPE_LIST) ^ 0xFF)
TYPE_BOOL_FALSE_INV = chr(ord(TYPE_BOOL_FALSE) ^ 0xFF)
TYPE_BOOL_TRUE_INV = chr(ord(TYPE_BOOL_TRUE) ^ 0xFF)
TYPE_NEGATIVE_FLOAT_INV = chr(ord(TYPE_NEGATIVE_FLOAT) ^ 0xFF)
TYPE_POSITIVE_FLOAT_INV = chr(ord(TYPE_POSITIVE_FLOAT) ^ 0xFF)
TYPE_UNICODE_INV = chr(ord(TYPE_UNICODE) ^ 0xFF)
TYPE_LARGEST_INV = chr(ord(TYPE_LARGEST) ^ 0xFF)


def load_key(cols, key):
    r = {}
    i = 0
    l = len(key)
    for col, dir in cols:
        v, i = _load_val(key, i, dir == 'd')
        if v is not None:
            r[col] = v
    return r


def _load_val(key, i, invert):
    c = key[i]
    i += 1
    if invert:
        c = chr(ord(c) ^ 0xFF)
    if c == TYPE_NONE:
        v = None
    elif c == TYPE_LIST:
        v = []
        try:
            while True:
                c, i = _load_val(key, i, invert)
                v.append(c)
        except StopIteration:
            pass
    elif c == TYPE_TERM:
        raise StopIteration()
    elif c == TYPE_BOOL_FALSE:
        v = False
    elif c == TYPE_BOOL_TRUE:
        v = True
    elif c == TYPE_NEGATIVE_FLOAT or c == TYPE_POSITIVE_FLOAT:
        if (c == TYPE_NEGATIVE_FLOAT and not invert) or invert:
            d = pack_long.unpack_from(key, i)[0]
            s = pack_long.pack(~d)
        else:
            s = key[i:i+8]
        i += 8
        v = pack_double.unpack(s)[0]
        l = long(v)
        if l == v:
            if l >= (-sys.maxint-1) and l <= sys.maxint:
                v = int(l)
            else:
                v = l
    elif c == TYPE_UNICODE:
        tmpbuf = StringIO()
        while True:
            c = key[i]
            if invert:
                c = chr(ord(c) ^ 0xFF)
            if c == '\x00':
                i += 1
                break
            elif c == '\x01':
                i += 1
                c = key[i]
                if invert:
                    c = chr(ord(c) ^ 0xFF)
            tmpbuf.write(c)
            i += 1
        v = unicode(tmpbuf.getvalue())
    else:
        raise KinetaError("Invalid type code: %s" % repr(c))
    return v, i


def dump_key(cols, key):
    return ''.join(_dump_vals(cols, key))


def _dump_vals(cols, key):
    for col, dir in cols:
        for s in _dump_val(key.get(col, None), dir == 'd'):
            yield s
    if isinstance(key, InvertKey):
        yield TYPE_NONE


def _dump_val(val, invert):
    if val is None:
        yield TYPE_NONE_INV if invert else TYPE_NONE
    else:
        t = _get_type(val)
        if t is list:
            yield TYPE_LIST_INV if invert else TYPE_LIST
            for c in val:
                for s in _dump_val(c, False):
                    yield s
            yield TYPE_TERM_INV if invert else TYPE_TERM
        elif t is bool:
            if invert:
                yield TYPE_BOOL_TRUE_INV if val else TYPE_BOOL_FALSE_INV
            else:
                yield TYPE_BOOL_TRUE if val else TYPE_BOOL_FALSE
        elif t is float:
            if val < 0:
                yield TYPE_NEGATIVE_FLOAT_INV if invert else \
                                                            TYPE_NEGATIVE_FLOAT
            else:
                yield TYPE_POSITIVE_FLOAT_INV if invert else \
                                                            TYPE_POSITIVE_FLOAT
            s = pack_double.pack(val)
            if (val < 0 and not invert) or invert:
                d = pack_long.unpack(s)[0]
                s = pack_long.pack(~d)
            yield s
        elif t is unicode:
            if isinstance(val, unicode):
                val = val.encode('utf-8')
            yield TYPE_UNICODE_INV if invert else TYPE_UNICODE
            if val.find(TYPE_TERM) == -1 and val.find('\x01') == -1:
                if invert:
                    for c in val:
                        yield chr(ord(c) ^ 0xFF)
                else:
                    yield val
            else:
                for c in val:
                    if c == TYPE_TERM or c == '\x01':
                        yield '\xFE' if invert else '\x01'
                    yield chr(ord(c) ^ 0xFF) if invert else c
            yield TYPE_TERM_INV if invert else TYPE_TERM
        elif t is Largest:
            yield TYPE_LARGEST_INV if invert else TYPE_LARGEST
        else:
            raise KinetaError("Invalid column value: %s" % val)
